最近在 App 裡面加上了紀錄使用者登山軌跡的功能後,上線的第一個週末 Firebase Crashlytics 就冒出來 20 個以上的 crash log,尿都快嚇出來了...
看了一下 log 後發現雖然引發的位置不同,但是全部都指向同一個問題
Caused by android.os.TransactionTooLargeException: data parcel size 2764576 bytes
at android.os.BinderProxy.transactNative(BinderProxy.java)
at android.os.BinderProxy.transact(BinderProxy.java:510)
... 以下省略
官方在 TransactionTooLargeException 的文檔中有提到 Binder transaction buffer 有 1MB 的限制
[color=#79bf18]
其中提到的 Binder 是 Android OS 中負責處理 Process 之間通信的機制(IPC, Inter-Process Communication), 當 Activity、Service、Broadcast Receiver 和 Content Provide 需要溝通時,就會需要 Binder 參與到其中
而我所碰到的狀況是紀錄了太多的軌跡點,當他需要切換到其他 Activity 時,我會透過 Intent + Parcelable 來傳遞軌跡點資料,而當 Intent 內容超過 1MB 時, OS 就會丟出 TransactionTooLargeException,因此必須找個方法讓資料可以完整的傳遞,同時不會引起 TransactionTooLargeException 的方法
而這個問題其實分析過後就是捨棄 Intent 傳遞資料,只需要有一個地方可以暫存這些資料,而需要的 Activity 可以取得,那這樣就可以解決上述的問題
而最多人使用的是 EventBus 來解,其中包括阿里巴巴的開發手冊都推荐使用 EventBus 來解大量數據傳遞的問題,因此應該可以很放心的使用 EventBus 來解吧...

其實在這之前我都沒有用過 EventBus 的經驗,但在了解過後發現如果只是用基礎功能,那其實還滿好上手的
在我看完 EventBus 的介紹後,很直覺的就是想到 LiveData 或只是 RxJava 之類的工具,他有Publisher 以及 Subscriber,Subscriber 和 Publisher 之間不用知道彼此的存在,只要 Subscriber 先跟 EventBus Manager 註冊要收哪類型的訊息,當 Publisher 發送相同類型的訊息到 EventBus Manager 時, Manager 就會負責轉交給那些有註冊的 Subscriber,而這個過程是不用 Binder 的介入的

根據上面的基礎,我們只要在 Activity 之間設定 Publisher 以及 Subscriber 就可以了,但在使用時有一個地方要注意,在這種 Event Base 的架構下,如果 Subcriber 在 Publisher 發送訊息後才去註冊,是沒辦法拿到資料的
例如:
- A Activity 發送訊息
- A Activity 退到背景,並啟動 B Activity
- B Activity 去註冊要收到 Event
- B Activity 並不會收到
那這是個滿常見的操作,那 EventBus 非常貼心的提供了一個叫作 Sticky Events 的方法,透過這個方法,可以讓比較晚註冊的 Activity 也可以收到,我覺得非常讚!!!
那實作上分成 3 個部份
我是建立了一個叫作 MessageEvent 的 sealed class,並在其中建立多個 data class 來區分訊息的類型
// MessageEvent.kt
sealed class MessageEvent {
data class MessageTrack(val track: Track) : MessageEvent()
data class MessageSearch(val text: String) : MessageEvent()
}
這邊可以看到我建立了兩個 data class,分別表示兩種類型的訊息
那 Subscriber 就可以像 EventBus 註冊需要收到上面哪些類型的訊息,那這邊有一個重點需要注意, Subscriber 的 Register 和 Unregister 需要由開發者自己控管,依照我的狀況我是在 onStart() 的時候 Register 並在 onStop() 的時候 Unregister
在需要收 Event 的 Method 需要加上 @Subscribe 告訴 EventBus,並且加上 sticky = true 才能收到已經被發過的 event ,而需要在 Method 中透 when 指定要收到哪種訊息,以及收到之後行為
// 要收訊息的 Activity
class HikeStatisticsActivity : AppCompatActivity() {
...
override fun onStart() {
super.onStart()
// 向 EventBus 註冊
EventBus.getDefault().register(this)
}
override fun onStop() {
super.onStop()
// 結束註冊
EventBus.getDefault().unregister(this)
}
// 告訴 EventBus 可以收到之前發出來的訊息,以及跑在 MAIN theread 上
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onResultReceived(event: MessageEvent) {
when (event) {
is MessageEvent.MessageHike -> {
// 指定收到的訊息以及之後的行為
...
}
}
}
...
}
最後就可以透過 Publisher 發送消息了,透過 postSticky 可以讓發送被保留住,因此在發送訊息後才啟動的 Activity 也可以收到 event 喔!
另外 Publisher 的 Activity 如果沒有要接收其他 Activity 的資料,是不需要在 onStart() 以及 onStop() 中向 EventBus 註冊的喔~
// 要發訊息的 Activity
class TrackingActivity : AppCompatActivity() {
...
private inner class StatisticsClickListener : View.OnClickListener {
override fun onClick(v: View?) {
val intent = Intent(context, HikeStatisticsActivity::class.java)
EventBus.getDefault().postSticky(MessageEvent.MessageHike(trackingData))
startActivity(intent)
}
}
...
}
透過 EventBus 這樣的機制,在程式撰寫上就可以做到
對於應用上也是有很大的彈性,雖然很方便,但這種 Event Base 撰寫上還是要做到儘量單一一點,否則一堆 Event 互相觸發、打來打去,在之後 Debug 也會感覺很困擾的!(曾經被 LiveData 循環觸發 Event 殘害過的人應該都有感...)
最後如果各位大大有更好的解法也歡迎留言分享喔~